﻿using System.Reflection;
using Microsoft.Practices.SharePoint.Common.Logging;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using SharePoint.Contrib.App.Settings;
using SharePoint.Contrib.Extensions;
using SharePoint.Contrib.InversionOfControl;
using SharePoint.Contrib.JobDefinitions;

namespace SharePoint.Contrib.Diagnostics
{
    /// <summary>
    /// Service for adding and removing DiagnosticsAreas and DiagnosticsCategories
    /// </summary>
    public class AreasAndCategoriesService
    {
        private SPFeature feature;
        private DiagnosticsAreaCollection SharePointAreas;
        private DiagnosticsAreaCollection InternalAreas;
        private bool persisted = false;

        private void Trace(string message, params object[] args)
        {
            string msg = string.Format("AreasAndCategoriesService: {0}", message);

            SPCServiceLocator.Current
                .InstanceOf<SPCDiagnosticsService>()
                .Trace(msg, args);
        }

        /// <summary>
        /// Creates a new instance of the <see cref="AreasAndCategoriesService"/>.
        /// </summary>
        /// <param name="feature">The feature has to be scoped for farm</param>
        public AreasAndCategoriesService(SPFeature feature)
        {
            if (feature.Definition.Scope != SPFeatureScope.Farm)
            {
                throw "AreasAndCategoriesService: The feature is not scoped for farm level which is the only scope that is supported."
                        .AsException();
            }
            this.feature = feature;
            this.SharePointAreas = new DiagnosticsAreaCollection(SPG.ConfigManager);
            this.InternalAreas = new DiagnosticsAreaCollection();

            Trace("Called from feature '{0}'.", this.feature.Name());
        }

        /// <summary>
        /// Adds DiagnosticsAreas and DiagnosticsCategories from classes that inherit from Area and from DiagnosticsAreaAttributes on the assembly calling this method.
        /// </summary>
        public void ApplyAreasAndCategoriesByConvention()
        {
            Trace("Applying DiagnosticsAreas and DiagnosticsCategories by attributes and inheritance -  by convention");
           
            Assembly caller = Assembly.GetCallingAssembly();

            var areas = caller.FindAllAreas();

            foreach (var area in areas)
            {
                Area(area.Name)
                    .AddCategories(area.Categories);
            }

            ApplyChanges();
        }

        /// <summary>
        /// Adds a new DiagnosticsArea if it doesn´t already exist
        /// </summary>
        /// <param name="name">The name of the area</param>
        public Area Area(string name)
        {
            Trace("Trying to find DiagnosticsArea '{0}'", name);

            DiagnosticsArea DiagnosticsArea = SharePointAreas[name];
            if (DiagnosticsArea == null)
            {
                Trace("DiagnosticsArea '{0}' was not found, creating it.", name);

                SharePointAreas.Add(new DiagnosticsArea { Name = name });
            }

            // add the area to the internal collection so we can remember the areas that was added by this feature.
            InternalAreas.Add(new DiagnosticsArea { Name = name });

            Area area = AreaFactory.Create(name);
            area.OnAddingCategory += HandleAddingCategory;

            return area;
        }
        
        /// <summary>
        /// Persists any new DiagnosticsAreas and/or DiagnosticsCategories to SharePoint
        /// </summary>
        public void ApplyChanges()
        {
            if (persisted == false) // ensure that we don´t call it more than once per feature
            {
                Trace("Calling ApplyChanges to persist the DiagnosticsAreaCollection in SharePoint");

                SharePointAreas.SaveConfiguration();
                EnsureDiagnosticsAreasAreRegistred();
                persisted = true;
            }
        }       
                
        /// <summary>
        /// Removes the categories and areas (if that area has no more categories after removal) that was made by the feature used to create the service.
        /// </summary>
        public void RemoveChanges()
        {
           
            var farm = feature.Scope<SPFarm>(SPFeatureScope.Farm);

            DiagnosticsAreaCollection DiagnosticsAreas = farm
                                                            .Settings()
                                                            .FindById<DiagnosticsAreaCollection>(this.feature.Identifier()).Value;
            
            foreach (var DiagnosticsArea in DiagnosticsAreas)
            {
                DiagnosticsArea area = SharePointAreas[DiagnosticsArea.Name];
                
                if (area != null)
                {
                    foreach (var DiagnosticsCategory in DiagnosticsArea.DiagnosticsCategories)
                    {
                        DiagnosticsCategory category = area.DiagnosticsCategories[DiagnosticsCategory.Name];

                        if (category != null)
                        {
                            Trace("Removing DiagnosticsCategory '{0}' from DiagnosticsArea '{1}'", category.Name, area.Name);
                            
                            area.DiagnosticsCategories.Remove(category);
                        }
                    }

                    if (area.DiagnosticsCategories.Count == 0)
                    {
                        Trace("Removing DiagnosticsArea '{0}' since it has no DiagnosticsCategories.", area.Name);
                        
                        SharePointAreas.Remove(area);
                    } 
                }
            }

            Trace("Calling SaveConfiguration to remove DiagnosticsCategories and DiagnosticsAreas");

            SharePointAreas.SaveConfiguration();
            EnsureDiagnosticsAreasAreUnRegistred();
        }

        private void EnsureDiagnosticsAreasAreUnRegistred()
        {
            Trace("Removing the application setting with id '{0}'", this.feature.Identifier());
            
            var farm = feature.Scope<SPFarm>(SPFeatureScope.Farm);

            farm
                .Settings()
                .Delete(this.feature.Identifier());
        }

        private void EnsureDiagnosticsAreasAreRegistred()
        {
            Trace("Adding the DiagnosticsAreaCollection to SharePoint as an application setting with id '{0}'", this.feature.Identifier());

            var farm = feature.Scope<SPFarm>(SPFeatureScope.Farm);
            farm
                .Settings()
                .Add(new Setting
                {
                    Id = this.feature.Identifier(),
                    Value = InternalAreas
                });

            EnsureConfiguredAreasRegistered();
        }

        private void EnsureConfiguredAreasRegistered()
        {
            Trace("Running SPJob 'EnsureConfiguredAreasRegistered' to ensure that windows event log has all the areas and categories.");

            var job = new EnsureConfiguredAreasRegistered();
            job.RunNow();
        }

        private void HandleAddingCategory(object sender, EventArgs<Category> e)
        {

            Area area = sender as Area;
            DiagnosticsArea DiagnosticsArea = SharePointAreas[area.Name];
            DiagnosticsCategory category = new DiagnosticsCategory 
            { 
                Name = e.Value.Name,
                Id = 0,
                EventSeverity = EventSeverity.Information,
                TraceSeverity = TraceSeverity.Medium
            };

            Trace("Trying to find DiagnosticsCategory '{0}' in DiagnosticsArea '{1}'", e.Value, DiagnosticsArea.Name);
  
            DiagnosticsCategory DiagnosticsCategory = DiagnosticsArea.DiagnosticsCategories[e.Value.Name];

            if (DiagnosticsCategory == null)
            {
                Trace("DiagnosticsCategory '{0}' was not found, adding it to DiagnosticsArea '{1}'.", e.Value, DiagnosticsArea.Name);

                DiagnosticsArea.DiagnosticsCategories.Add(category);
            }

            // add the category to the internal collection so we can remember the categories that was added by this feature.
            InternalAreas[DiagnosticsArea.Name]
                .DiagnosticsCategories
                .Add(category);
        }


    }

    
}
